Skip to content

feat: add support for Firestore Pipeline#292

Open
Lyokone wants to merge 3 commits into
mainfrom
feat/pipeline
Open

feat: add support for Firestore Pipeline#292
Lyokone wants to merge 3 commits into
mainfrom
feat/pipeline

Conversation

@Lyokone

@Lyokone Lyokone commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Add support for Firestore Pipeline

How to use

final snapshot = await firestore
    .pipeline()
    .collection('books')
    .where(Expression.field('active').equalValue(true))
    .sort([Expression.field('price').ascending()])
    .select([
      Expression.field('title'),
      Expression.field('price'),
      Expression.field('title').toUpperCase().as('upperTitle'),
      Expression.field('tags').arrayLength().as('tagCount'),
    ])
    .limit(10)
    .execute();
for (final result in snapshot.results) {
  print(result.data());
}

Aggregates

Aggregate stages use aliased aggregate expressions:

final snapshot = await firestore
    .pipeline()
    .collection('books')
    .where(Expression.field('active').equalValue(true))
    .aggregate([
      Expression.field('price').sum().as('totalPrice'),
      Expression.field('rating').average().as('averageRating'),
      PipelineFunctions.count().as('bookCount'),
    ])
    .execute();
final data = snapshot.results.single.data();
print(data);

Expressions

Use Expression.field, Expression.constant, and Expression.variable to
build expressions. Most helpers are also available as fluent methods:

final expression = Expression.field('createdAt')
    .timestampSubtract('day', 7)
    .timestampToUnixSeconds()
    .as('createdSeconds');

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces Firestore Pipeline operations to the google_cloud_firestore package, enabling server-side projections, expressions, aggregates, and vector search, complete with comprehensive E2E and unit tests. The review feedback highlights several key improvement opportunities: reverting a breaking change to the DistanceMeasure enum by converting values to lowercase locally within the pipeline execution, optimizing performance by extracting a frequently compiled regular expression into a file-level constant, and adding as well as exporting missing top-level comparison helpers (lessThan and greaterThan) to ensure API completeness.

Comment thread packages/google_cloud_firestore/lib/src/pipeline.dart
Comment thread packages/google_cloud_firestore/lib/src/pipeline.dart Outdated
Comment thread packages/google_cloud_firestore/lib/src/pipeline.dart
Comment thread packages/google_cloud_firestore/lib/google_cloud_firestore.dart
@github-actions

Copy link
Copy Markdown

Coverage Report

✅ Coverage 71.95% meets 40% threshold

Total Coverage: 71.95%
Lines Covered: 5488/7627

Package Breakdown

Package Coverage
google_cloud_firestore 71.31%
firebase_admin_sdk 72.66%

Minimum threshold: 40%

@demolaf demolaf self-requested a review June 30, 2026 15:03

/// The starting point for constructing Firestore Pipeline operations.
@immutable
final class PipelineSource {

@demolaf demolaf Jul 3, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@demolaf demolaf left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PipelineFunctions accepts Object? but only Expression.field() actually works without a fieldOrExpression-style check — untested since every call site already used field(). We should either commit to requiring Expression.field() explicitly or handle the coercion like Node's fieldOrExpression.


/// STARTS_WITH string function.
static PipelineBooleanExpression startsWith(Object? value, Object? prefix) {
return _bool('starts_with', [value, prefix]);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since value is typed as Object?, we should check at runtime whether it was passed as a String and wrap it with field(value) so it's encoded as a field reference.

Without that check, the string is encoded as a literal value instead of a field reference — so the expression compares against the fixed text 'title', not the value of the title field.

Example (Bug) — this looks like it filters on the title field, but instead compares the literal text "title" against "Harry":

await firestore
    .pipeline()
    .collection('books')
    .where(PipelineFunctions.startsWith('title', 'Harry'))
    .execute();

This problem also exists in other methods across PipelineFunctions e.g. equal, lessThan, arrayContains, mapGet, and others

}

/// STARTS_WITH string function.
static PipelineBooleanExpression startsWith(Object? value, Object? prefix) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should rename value to fieldName which is more descriptive and matches node.

Comment on lines +460 to +462
static PipelineBooleanExpression equal(Object? left, Object? right) {
return _bool('equal', [left, right]);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same issue here, we should check if left is String and wrap with field(left).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants